package compiler.common;

import java.util.List;
import java.util.ArrayList;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;

/**
 * Environment class used during type and semantic checking
 * @author Michal Anglart
 */
public class Environment { 
  List<TypeTC> declaredTypes;          // list of types declared
  Map<String, String> variables;     // map from identifier to a type name
  List<String> errors;               // list of collected errors
  List<Method> globalFuns;           // list of global funs like sin, cos etc
  
  Map<String, String> restore_point; // it used to save variables map, and restore it
  String insideType;                 // name of class we are in (in tree)

  // masks/flags
  public static final int EQUAL=1;   
  public static final int NONEQ=2;
  public static final int LESS=4;
  public static final int LEQ=8;
  public static final int GREAT=16;
  public static final int GREQ=32;
  public static final int PLUS=64;
  public static final int MINUS=128;
  public static final int MULT=256;
  public static final int DIV=512;
  public static final int MOD=1024;
  
  
  /**
   * Standard constructor
   */
  public Environment() {
    this.declaredTypes = new ArrayList<TypeTC>();
    
    this.prepareType("integer", EQUAL | NONEQ | LESS | LEQ | 
        GREAT | GREQ | PLUS | MINUS | MULT | DIV | MOD);
    this.prepareType("array of integer", 0);
    this.prepareType("real", EQUAL | NONEQ | LESS | LEQ | 
        GREAT | GREQ | PLUS | MINUS | MULT | DIV);
    this.prepareType("array of real", 0);
    this.prepareType("boolean", EQUAL | NONEQ);
    this.prepareType("array of boolean", 0);
    this.prepareType("string", EQUAL | NONEQ | PLUS);
    this.prepareType("array of string", 0);
    this.prepareType("char", EQUAL | NONEQ | LESS | LEQ | 
        GREAT | GREQ);
    this.prepareType("array of char", 0);
        
    this.errors = new ArrayList<String>();
    this.variables = new HashMap<String, String>();
    this.globalFuns = new ArrayList<Method>();
    
    Method m = new Method("sin");
    m.addArgType("real");
    m.setRetType("real");
    this.globalFuns.add(m);
    
    m = new Method("cos");
    m.addArgType("real");
    m.setRetType("real");
    this.globalFuns.add(m);
    
    m = new Method("tan");
    m.addArgType("real");
    m.setRetType("real");
    this.globalFuns.add(m);

    m = new Method("log");
    m.addArgType("real");
    m.setRetType("real");
    this.globalFuns.add(m);

    m = new Method("pow");
    m.addArgType("real");
    m.addArgType("real");
    m.setRetType("real");
    this.globalFuns.add(m);
  }
  
  public List<Method> getGlobalFuns() {
    return this.globalFuns;
  }
  
  public boolean hasGlobalFun(String name, List<String> argtypes) {
    for (Method m : this.globalFuns) {
      if(m.getName().equals(name) && m.getArgTypes().equals(argtypes))
        return true;
    }
    return false;
  }
  
  public String getGlobalFunRet(String name, List<String> argtypes) {
    for (Method m : this.globalFuns) {
      if(m.getName().equals(name) && m.getArgTypes().equals(argtypes))
        return m.getRetType();
    }
    return null;
  }
  
  public List<TypeTC> getTypes() {
    return this.declaredTypes;
  }
  
  public Map<String, String> makeMethodsSignatures() {
    
    HashMap<String, String> map = new HashMap<String, String>();
    
    for(int i = 10; i < this.declaredTypes.size(); i++) {
      HashMap<String, String> tmp = new HashMap<String, String>();
      TypeTC t = this.declaredTypes.get(i);
      tmp = (HashMap<String,String>)t.makeMethodSig();
      Set<String> keys = tmp.keySet();
      
      for(String k : keys) {
        map.put(k, tmp.get(k));
      }
    }   
    
    return map;
  }
  
  /**
   * Checks if all declared methods has its implementation
   * @param typename Name of type to check
   * @throws Exception Thrown when there is some missing implementations
   */
  public void checkImplementations(String typename) throws Exception {
    TypeTC t = this.findType(typename);
    boolean r = t.checkImplementations();
    if(r)
      return;
    else
      throw new Exception("Type " + typename + " have some missing " +
          "methods implementation");
  }
  
  /**
   * Setter for insideType
   */
  public void setInsideType(String intype)
  {
    this.insideType = intype;
  }
  
  /**
   * Getter of insideType
   * @return Name of class
   */
  public String getInsideType()
  {
    return this.insideType;
  }
  
  /**
   * Saves actual variables map as restore point
   */
  public void saveVars()
  {
    this.restore_point = new HashMap<String, String>(this.variables);
  }
  
  /**
   * Restores variable map from restore point
   */
  public void restoreVars()
  {
    this.variables = new HashMap<String, String>(this.restore_point);
    this.restore_point = null;
  }
  
  /**
   * Checks whether type is already defined
   * @param typename Type to check
   * @return true iff type was defined 
   */
  public boolean isDefined(String typeName)
  {    
    for(TypeTC type : declaredTypes) {
      if(typeName.equals(type.getName()))
        return true;
    }
    return false;
  }
  
  /**
   * Gets type of given name
   * @param typename
   * @return
   */
  public TypeTC findType(String typename) {
    for(TypeTC t : this.declaredTypes) {
      if(t.isEqual(typename))
        return t;
    }
    return null;
  }
  
  /**
   * Checks if address fields is proper
   * @param path
   * @param type
   * @return Type of last field in adress path
   * @throws Exception
   */
  public String checkProperField(List<String> path, String type) throws Exception {
    TypeTC t;
    t = this.findType(type);
    if(t == null)
        throw new Exception("Type '" + type + "' is not defined");
    path = path.subList(1, path.size());
    for(String s : path) {
      if( ! t.hasField(s) )
        throw new Exception("There is no public field '" + s + "' in '" + type + "'");
      else
      {
        type = t.getFieldType(s);
        t = this.findType(type);
      }
    }
    return type;
  }
  
  /**
   * Adds new type to environment
   * @param type Type to add
   */
  public void addType(TypeTC type) throws Exception {
    if(this.isDefined(type.getName()))
      throw new Exception("Error: type '" + 
          type.getName() + "' was already defined");

    this.declaredTypes.add(type);
  }

  /**
   * Binds variable with type
   * @param var name of variable 
   * @param type type of variable
   */
  public void addVariable(String variableName, String typeName) throws Exception {
    if(this.variables.containsKey(variableName)) 
    {
      throw new Exception("Error: redefinition of '" + 
          variableName + "' variable");
    } 
    else {
      this.variables.put(variableName, typeName);
    }
  }

  /**
   * Checks whether variable was declared
   * @param var variable name
   * @return true iff variable was declared
   */
  public boolean isDeclared(String variableName) {
    return this.variables.containsKey(variableName);
  }
  
  /**
   * Get type name binded with specific variable
   * @param var name of variable
   * @return name of type
   */
  public String getVariableTypeName(String variableName) throws Exception {
    if(this.variables.containsKey(variableName))
      return this.variables.get(variableName);
    else
      throw new Exception("Unbound variable '" + variableName + "'");
  }
  
  /**
   * Adds new error to list
   * @param error
   */
  public void addError(String error) {
    this.errors.add(error);
  }
  
  /**
   * Gets error list
   * @return List of errors
   */
  public List<String> getErrors() {
    return this.errors;
  }

  /**
   * Reset environment errors list
   */
  public void resetErrors() {
    this.errors = new ArrayList<String>();
  }
  
  /**
   * Reset environment variable map
   */
  public void resetVars() {
    this.variables = new HashMap<String, String>();
  }
  
  /**
   * Reset environment errors list and variable map
   */
  public void reset() {
    this.errors = new ArrayList<String>();
    this.variables = new HashMap<String, String>();
  }
  
  /**
   * Test if specified type has given method
   * @param type
   * @param method
   * @return true iff method is implemented in type
   */
  public boolean hasMethod(String type, String method, List<String> argtypes) {
    TypeTC t = this.findType(type);
    return t.hasMethod(method, argtypes);
  }
  
  /**
   * Prepares specified types (used to prepare standard types)
   * @param typename
   * @param flags It is used to specify which operation should be implemented by type
   */
  private void prepareType(String typename, int flags) {
    Method m;
    TypeTC t;
    try {
      t = new TypeTC(typename);
      if( (flags & EQUAL) == EQUAL) {
        m = createOperator("=", typename, "boolean");
        t.addMethod(m);        
      }
      if( (flags & NONEQ) == NONEQ) {
        m = createOperator("!=", typename, "boolean");
        t.addMethod(m);
      }
      if( (flags & LESS) == LESS) {
        m = createOperator("<", typename, "boolean");
        t.addMethod(m);
      }
      if( (flags & LEQ) == LEQ) {
        m = createOperator("<=", typename, "boolean");
        t.addMethod(m);
      }
      if( (flags & GREAT) == GREAT) {
        m = createOperator(">", typename, "boolean");
        t.addMethod(m);
      }
      if( (flags & GREQ) == GREQ) {
        m = createOperator(">=", typename, "boolean");
        t.addMethod(m);
      }
      if( (flags & PLUS) == PLUS) {
        m = createOperator("+", typename, typename);
        t.addMethod(m);
      }
      if( (flags & MINUS) == MINUS) {
        m = createOperator("-", typename, typename);
        t.addMethod(m);
      }
      if( (flags & MULT) == MULT) {
        m = createOperator("*", typename, typename);
        t.addMethod(m);
      }
      if( (flags & DIV) == DIV) {
        m = createOperator("div", typename, typename);
        t.addMethod(m);
      }
      if( (flags & MOD) == MOD) {
        m = createOperator("mod", typename, typename);
        t.addMethod(m);
      }
      this.declaredTypes.add(t);
    }
    catch(Exception e) {       
    }  
  }
  
  /**
   * Creates new method - operator of given name (e.g. +) argument type and return type
   * @param op
   * @param arg
   * @param ret
   * @return
   */
  private Method createOperator(String op, String arg, String ret) {
    Method m = new Method(op);
    m.addArgType(arg);
    m.addArgType(arg);
    m.setRetType(ret);
    m.setModifier(true);
    m.setOperator(true); 
    return m;
  }
  
}